5.6 公开或未公开的标识符

要想设计出好的 API,需要使用某种规则来控制声明后的标识符的可见性。Go 语言支持从包里公开或者隐藏标识符。通过这个功能,让用户能按照自己的规则控制标识符的可见性。在第3章讨论包的时候,谈到了如何从一个包引入标识符到另一个包。有时候,你可能不希望公开包里的某个类型、函数或者方法这样的标识符。在这种情况,需要一种方法,将这些标识符声明为包外不可见,这时需要将这些标识符声明为未公开的。

让我们用一个示例程序来演示如何隐藏包里未公开的标识符,如代码清单5-64所示。

代码清单5-64 listing64/

counters/counters.go
-----------------------------------------------------------------------
01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int
listing64.go
-----------------------------------------------------------------------
01 // 这个示例程序展示无法从另一个包里
02 // 访问未公开的标识符
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing64/counters"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 创建一个未公开的类型的变量
14   // 并将其初始化为10
15   counter := counters.alertCounter(10)
16
17   // ./listing64.go:15: 不能引用未公开的名字
18   //          counters.alertCounter
19   // ./listing64.go:15: 未定义:counters.alertCounter
20
21   fmt.Printf("Counter: %d\n", counter)
22 }

这个示例程序有两个代码文件。一个代码文件名字为counters.go,保存在 counters 包里;另一个代码文件名字为listing64.go,导入了 counters 包。让我们先从 counters 包里的代码开始,如代码清单5-65所示。

代码清单5-65  counters /counters.go

01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int

代码清单5-65展示了只属于 counters 包的代码。你可能会首先注意到第02行。直到现在,之前所有的示例程序都使用了 package main ,而这里用到的是 package counters 。当要写的代码属于某个包时,好的实践是使用与代码所在文件夹一样的名字作为包名。所有的Go工具都会利用这个习惯,所以最好遵守这个好的实践。

counters 包里,我们在第06行声明了唯一一个名为 alertCounter 的标识符。这个标识符是一个使用 int 作为基础类型的类型。需要注意的是,这是一个未公开的标识符。

当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。如果一个标识符以大写字母开头,这个标识符就是公开的,即被包外的代码可见。让我们看一下导入这个包的代码,如代码清单5-66所示。

代码清单5-66 listing64.go

01 // 这个示例程序展示无法从另一个包里
02 // 访问未公开的标识符
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing64/counters"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 创建一个未公开的类型的变量
14   // 并将其初始化为10
15   counter := counters.alertCounter(10)
16
17   // ./listing64.go:15: 不能引用未公开的名字
18   //                       counters.alertCounter
19   // ./listing64.go:15: 未定义:counters.alertCounter
20
21   fmt.Printf("Counter: %d\n", counter)
22 }

代码清单5-66中的listing64.go的代码在第03行声明了 main 包,之后在第08行导入了 counters 包。在这之后,我们跳到 main 函数里的第15行,如代码清单5-67所示。

代码清单5-67 listing64.go:第13到19行

13   // 创建一个未公开的类型的变量
14   // 并将其初始化为10
15   counter := counters.alertCounter(10)
16
17   // ./listing64.go:15: 不能引用未公开的名字
18   //                       counters.alertCounter
19   // ./listing64.go:15: 未定义:counters.alertCounter

在代码清单5-67的第15行,代码试图创建未公开的 alertCounter 类型的值。不过这段代码会造成第15行展示的编译错误,这个编译错误表明第15行的代码无法引用 counters.alertCounter 这个未公开的标识符。这个标识符是未定义的。

由于 counters 包里的 alertCounter 类型是使用小写字母声明的,所以这个标识符是未公开的,无法被listing64.go的代码访问。如果我们把这个类型改为用大写字母开头,那么就不会产生编译器错误。让我们看一下新的示例程序,如代码清单5-68所示,这个程序在 counters 包里实现了工厂函数。

代码清单5-68 listing68/

counters/counters.go
-----------------------------------------------------------------------
01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int
07
08 // New创建并返回一个未公开的
09 // alertCounter类型的值
10 func New(value int) alertCounter {
11   return alertCounter(value)
12 }
listing68.go
-----------------------------------------------------------------------
01 // 这个示例程序展示如何访问另一个包的未公开的
02 // 标识符的值
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing68/counters"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 使用counters包公开的New函数来创建
14   // 一个未公开的类型的变量
15   counter := counters.New(10)
16
17   fmt.Printf("Counter: %d\n", counter)
18 }

这个例子已经修改为使用工厂函数来创建一个未公开的 alertCounter 类型的值。让我们先看一下 counters 包的代码,如代码清单5-69所示。

代码清单5-69  counters /counters.go

01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int
07
08 // New创建并返回一个未公开的
09 // alertCounter类型的值
10 func New(value int) alertCounter {
11   return alertCounter(value)
12 }

代码清单5-69展示了我们对 counters 包的改动。 alertCounter 类型依旧是未公开的,不过现在在第10行增加了一个名为 New 的新函数。将工厂函数命名为 New 是Go语言的一个习惯。这个 New 函数做了些有意思的事情:它创建了一个未公开的类型的值,并将这个值返回给调用者。让我们看一下listing68.go的 main 函数,如代码清单5-70所示。

代码清单5-70 listing68.go

11 // main是应用程序的入口
12 func main() {
13   // 使用counters包公开的New函数来创建
14   // 一个未公开的类型的变量
15   counter := counters.New(10)
16
17   fmt.Printf("Counter: %d\n", counter)
18 }

在代码清单5-70的第15行,可以看到对 counters 包里 New 函数的调用。这个 New 函数返回的值被赋给一个名为 counter 的变量。这个程序可以编译并且运行,但为什么呢? New 函数返回的是一个未公开的 alertCounter 类型的值,而 main 函数能够接受这个值并创建一个未公开的类型的变量。

要让这个行为可行,需要两个理由。第一,公开或者未公开的标识符,不是一个值。第二,短变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。永远不能显式创建一个未公开的类型的变量,不过短变量声明操作符可以这么做。

让我们看一个新例子,这个例子展示了这些可见的规则是如何影响到结构里的字段,如代码清单5-71所示。

代码清单5-71 listing71/

entities/entities.go
-----------------------------------------------------------------------
01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // User在程序里定义一个用户类型
06 type User struct {
07   Name string
08   email string
09 }
listing71.go
-----------------------------------------------------------------------
01 // 这个示例程序展示公开的结构类型中未公开的字段
02 // 无法直接访问
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing71/entities"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 创建entities包中的User类型的值
14   u := entities.User{
15     Name: "Bill",
16     email: "bill@email.com",
17   }
18
19   // ./example69.go:16: 结构字面量中结构entities.User
20   //          的字段’email’未知
21
22   fmt.Printf("User: %v\n", u)
23 }

代码清单5-71中的代码有一些微妙的变化。现在我们有一个名为 entities 的包,声明了名为 User 的结构类型,如代码清单5-72所示。

代码清单5-72  entities /entities.go

01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // User在程序里定义一个用户类型
06 type User struct {
07   Name string
08   email string
09 }

代码清单5-72的第06行中的 User 类型被声明为公开的类型。 User 类型里声明了两个字段,一个名为 Name 的公开的字段,一个名为 email 的未公开的字段。让我们看一下listing71.go的代码,如代码清单5-73所示。

代码清单5-73 listing71.go

01 // 这个示例程序展示公开的结构类型中未公开的字段
02 // 无法直接访问
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing71/entities"
09 )
10
11 // main是程序的入口
12 func main() {
13   // 创建entities包中的User类型的值
14   u := entities.User{
15     Name: "Bill",
16     email: "bill@email.com",
17   }
18
19   // ./example69.go:16: 结构字面量中结构entities.User
20   //          的字段'email'未知
21
22   fmt.Printf("User: %v\n", u)
23 }

代码清单5-73的第08行导入了 entities 包。在第14行声明了 entities 包中的公开的类型 User 的名为 u 的变量,并对该字段做了初始化。不过这里有一个问题。第16行的代码试图初始化未公开的字段 email ,所以编译器抱怨这是个未知的字段。因为 email 这个标识符未公开,所以它不能在 entities 包外被访问。

让我们看最后一个例子,这个例子展示了公开和未公开的内嵌类型是如何工作的,如代码清单5-74所示。

代码清单5-74 listing74/

entities/entities.go
-----------------------------------------------------------------------
01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // user在程序里定义一个用户类型
06 type user struct {
07   Name string
08   Email string
09 }
10
11 // Admin在程序里定义了管理员
12 type Admin struct {
13   user  // 嵌入的类型是未公开的
14   Rights int
15 }
listing74.go
-----------------------------------------------------------------------
01 // 这个示例程序展示公开的结构类型中如何访问
02 // 未公开的内嵌类型的例子
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing74/entities"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 创建entities包中的Admin类型的值
14   a := entities.Admin{
15     Rights: 10,
16   }
17
18   // 设置未公开的内部类型的
19   // 公开字段的值
20   a.Name = "Bill"
21   a.Email = "bill@email.com"
22
23   fmt.Printf("User: %v\n", a)
24 }

现在,在代码清单5-74里, entities 包包含两个结构类型,如代码清单5-75所示。

代码清单5-75  entities /entities.go

01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // user在程序里定义一个用户类型
06 type user struct {
07   Name string
08   Email string
09 }
10
11 // Admin在程序里定义了管理员
12 type Admin struct {
13   user  // 嵌入的类型未公开
14   Rights int
15 }

在代码清单5-75的第06行,声明了一个未公开的结构类型 user 。这个类型包括两个公开的字段 NameEmail 。在第12行,声明了一个公开的结构类型 AdminAdmin 有一个名为 Rights 的公开的字段,而且嵌入一个未公开的 user 类型。让我们看一下listing74.go的 main 函数,如代码清单5-76所示。

代码清单5-76 listing74.go:第11到24行

11 // main是应用程序的入口
12 func main() {
13   // 创建entities包中的Admin类型的值
14   a := entities.Admin{
15     Rights: 10,
16   }
17
18   // 设置未公开的内部类型的
19   // 公开字段的值
20   a.Name = "Bill"
21   a.Email = "bill@email.com"
22
23   fmt.Printf("User: %v\n", a)
24 }

让我们从代码清单5-76的第14行的 main 函数开始。这个函数创建了 entities 包中的 Admin 类型的值。由于内部类型 user 是未公开的,这段代码无法直接通过结构字面量的方式初始化该内部类型。不过,即便内部类型是未公开的,内部类型里声明的字段依旧是公开的。既然内部类型的标识符提升到了外部类型,这些公开的字段也可以通过外部类型的字段的值来访问。

因此,在第20行和第21行,来自未公开的内部类型的字段 NameEmail 可以通过外部类型的变量 a 被访问并被初始化。因为 user 类型是未公开的,所以这里没有直接访问内部类型。

results matching ""

    No results matching ""